<template>
  <div class="vue-pull-to-wrapper"
       :style="{ height: wrapperHeight, transform: `translate3d(0, ${diff}px, 0)` }">
    <div v-if="topLoadMethod"
         :style="{ height: `${topBlockHeight}px`, marginTop: `${-topBlockHeight}px` }"
         class="action-block">
      <slot name="top-block"
            :state="state"
            :state-text="topText"
            :trigger-distance="_topConfig.triggerDistance"
            :diff="diff">
        <p class="default-text">{{ topText }}</p>
      </slot>
    </div>
    <div class="scroll-container">
      <slot></slot>
    </div>
    <div v-if="bottomLoadMethod"
         :style="{ height: `${bottomBlockHeight}px`, marginBottom: `${-bottomBlockHeight}px` }"
         class="action-block">
      <slot name="bottom-block"
            :state="state"
            :state-text="bottomText"
            :trigger-distance="_bottomConfig.triggerDistance"
            :diff="diff">
        <p class="default-text">{{ bottomText }}</p>
      </slot>
    </div>
  </div>
</template>

<script>
  const TOP_DEFAULT_CONFIG = {
    pullText: '下拉刷新',
    triggerText: '释放更新',
    loadingText: '加载中...',
    doneText: '加载完成',
    failText: '加载失败',
    loadedStayTime: 400,
    stayDistance: 50,
    triggerDistance: 70
  };

  const BOTTOM_DEFAULT_CONFIG = {
    pullText: '上拉加载',
    triggerText: '释放更新',
    loadingText: '加载中...',
    doneText: '加载完成',
    failText: '加载失败',
    loadedStayTime: 400,
    stayDistance: 50,
    triggerDistance: 70
  };
  export default {
    props: {
      distanceIndex: {//滑动的阀值（值越大滑动的速度越慢）
        type: Number,
        default: 2
      },
      topBlockHeight: {//顶部在滚动容器外的块级元素区域高度
        type: Number,
        default: 50
      },
      bottomBlockHeight: {//底部在滚动容器外的块级元素区域高度
        type: Number,
        default: 50
      },
      wrapperHeight: {//滚动容器的高度
        type: String,
        default: '100%'
      },
      topLoadMethod: {//顶部下拉时执行的方法
        type: Function
      },
      bottomLoadMethod: {//底部上拉时执行的方法
        type: Function
      },
      isThrottleTopPull: {//是否截流top-pull事件的触发以保证性能，如果需要实时触发设为false
        type: Boolean,
        default: true
      },
      isThrottleBottomPull: {//是否截流bottom-pull事件的触发以保证性能，如果需要实时触发设为false
        type: Boolean,
        default: true
      },
      isThrottleScroll: {//是否截流scroll事件的触发以保证性能，如果需要实时触发设为false
        type: Boolean,
        default: true
      },
      isTopBounce: {//是否启用下拉回弹效果
        type: Boolean,
        default: true
      },
      isBottomBounce: {//是否启用上拉回弹效果
        type: Boolean,
        default: true
      },
      topConfig: {//滚动容器顶部信息的一些配置
        type: Object,
        default: () => {
          return {};
        }
      },
      bottomConfig: {//滚动容器底部信息的一些配置
        type: Object,
        default: () => {
          return {};
        }
      }
    },
    data() {
      return {
        scrollEl: null,//挂载节点
        startScrollTop: 0,//到顶部的距离
        startY: 0,//touchstart Y的位置
        currentY: 0,//touchmove Y的位置
        distance: 0,//滑动的距离
        direction: 0,//方向 distance大于0为down、小于0为up
        diff: 0,//滑动差值(触发加载时才有值)
        beforeDiff: 0,
        topText: '',//顶部加载文字
        bottomText: '',//底部加载文字
        state: '',//pull、down、loading 状态
        bottomReached: false,//到达底部标识
        throttleEmitTopPull: null,//下拉节流
        throttleEmitBottomPull: null,//上拉节流
        throttleEmitScroll: null,//滚动节流
        throttleOnInfiniteScroll: null,//滚动底部加载更多节流
        pageY: 0,//用于记录当前页面滚动位置 keep-alive
      };
    },
    computed: {
      _topConfig: function () {
        return Object.assign({}, TOP_DEFAULT_CONFIG, this.topConfig);
      },
      _bottomConfig: function () {
        return Object.assign({}, BOTTOM_DEFAULT_CONFIG, this.bottomConfig);
      }
    },
    watch: {
      state(val) {
        if (this.direction === 'down') {
          this.$emit('top-state-change', val);
        } else {
          this.$emit('bottom-state-change', val);
        }
      }
    },
    mounted() {
      this.init();
    },
    activated(){
      this.scrollEl.scrollTop = this.pageY;
    },
    methods: {
      init() {
        this.createThrottleMethods();
        this.scrollEl = this.$el.querySelector('.scroll-container');
        this.bindEvents();
      },
      createThrottleMethods() {
        this.throttleEmitTopPull = this.throttleEmit(200, 300, 'top-pull');
        this.throttleEmitBottomPull = this.throttleEmit(200, 300, 'bottom-pull');
        this.throttleEmitScroll = this.throttleEmit(100, 150, 'scroll');
        this.throttleOnInfiniteScroll = this.throttle(this.onInfiniteScroll, 400);
      },
      throttleEmit(delay, mustRunDelay = 0, eventName) {
        const throttleMethod = function () {
          const args = [...arguments];
          args.unshift(eventName);
          this.$emit.apply(this, args);
        };
        return this.throttle(throttleMethod, delay, mustRunDelay);
      },
      bindEvents() {
        this.scrollEl.addEventListener('touchstart', this.handleTouchStart);
        this.scrollEl.addEventListener('touchmove', this.handleTouchMove);
        this.scrollEl.addEventListener('touchend', this.handleTouchEnd);
        this.scrollEl.addEventListener('scroll', this.handleScroll);
      },
      //节流
      throttle(fn, delay, mustRunDelay = 0) {
        let timer = null;
        let tStart;
        return function () {
          const context = this;
          const args = arguments;
          const tCurr = +new Date();
          clearTimeout(timer);
          if (!tStart) {
            tStart = tCurr;
          }
          if (mustRunDelay !== 0 && tCurr - tStart >= mustRunDelay) {
            fn.apply(context, args);
            tStart = tCurr;
          } else {
            timer = setTimeout(function () {
              fn.apply(context, args);
            }, delay);
          }
        };
      },
      onInfiniteScroll() {
        if (this.checkBottomReached()) {
          this.$emit('infinite-scroll');
        }
      },
      checkBottomReached() {
        this.pageY = this.scrollEl.scrollTop;
        return this.scrollEl.scrollTop + this.scrollEl.offsetHeight + 1 >= this.scrollEl.scrollHeight;
      },
      handleTouchStart(event) {
        this.startY = event.touches[0].clientY;
        this.beforeDiff = this.diff;
        this.startScrollTop = this.scrollEl.scrollTop;
        this.bottomReached = this.checkBottomReached();
      },
      handleTouchMove(event) {
        this.currentY = event.touches[0].clientY;
        this.distance = (this.currentY - this.startY) / this.distanceIndex + this.beforeDiff;
        this.direction = this.distance > 0 ? 'down' : 'up';
        if (this.startScrollTop === 0 && this.direction === 'down' && this.isTopBounce) {
          event.preventDefault();
          event.stopPropagation();
          this.diff = this.distance;
          this.isThrottleTopPull ? this.throttleEmitTopPull(this.diff) : this.$emit('top-pull', this.diff);
          if (typeof this.topLoadMethod !== 'function') return;
          if (this.distance < this._topConfig.triggerDistance && this.state !== 'pull' && this.state !== 'loading') {
            this.actionPull();
          } else if (this.distance >= this._topConfig.triggerDistance && this.state !== 'trigger' && this.state !== 'loading') {
            this.actionTrigger();
          }
        } else if (this.bottomReached && this.direction === 'up' && this.isBottomBounce) {
          event.preventDefault();
          event.stopPropagation();
          this.diff = this.distance;
          this.isThrottleBottomPull ? this.throttleEmitBottomPull(this.diff) : this.$emit('bottom-pull', this.diff);
          if (typeof this.bottomLoadMethod !== 'function') return;
          if (Math.abs(this.distance) < this._bottomConfig.triggerDistance &&
            this.state !== 'pull' && this.state !== 'loading') {
            this.actionPull();
          } else if (Math.abs(this.distance) >= this._bottomConfig.triggerDistance &&
            this.state !== 'trigger' && this.state !== 'loading') {
            this.actionTrigger();
          }
        }
      },
      handleTouchEnd() {
        if (this.diff !== 0) {
          if (this.state === 'trigger') {
            this.actionLoading();
            return;
          }
          // pull cancel
          this.scrollTo(0);
        }
      },
      handleScroll(event) {
        this.isThrottleScroll ? this.throttleEmitScroll(event) : this.$emit('scroll', event);
        this.throttleOnInfiniteScroll();
      },
      scrollTo(y, duration = 200) {
        this.$el.style.transition = `${duration}ms`;
        this.diff = y;
        setTimeout(() => {
          this.$el.style.transition = '';
        }, duration);
      },
      actionPull() {
        this.state = 'pull';
        this.direction === 'down'
          ? this.topText = this._topConfig.pullText
          : this.bottomText = this._bottomConfig.pullText;
      },
      actionTrigger() {
        this.state = 'trigger';
        this.direction === 'down'
          ? this.topText = this._topConfig.triggerText
          : this.bottomText = this._bottomConfig.triggerText;
      },
      actionLoading() {
        this.state = 'loading';
        if (this.direction === 'down') {
          this.topText = this._topConfig.loadingText;
          /* eslint-disable no-useless-call */
          this.topLoadMethod.call(this, this.actionLoaded);
          this.scrollTo(this._topConfig.stayDistance);
        } else {
          this.bottomText = this._bottomConfig.loadingText;
          this.bottomLoadMethod.call(this, this.actionLoaded);
          this.scrollTo(-this._bottomConfig.stayDistance);
        }
      },
      actionLoaded(loadState = 'done') {
        this.state = `loaded-${loadState}`;
        let loadedStayTime;
        if (this.direction === 'down') {
          this.topText = loadState === 'done'
            ? this._topConfig.doneText
            : this._topConfig.failText;
          loadedStayTime = this._topConfig.loadedStayTime;
        } else {
          this.bottomText = loadState === 'done'
            ? this._bottomConfig.doneText
            : this._bottomConfig.failText;
          loadedStayTime = this._bottomConfig.loadedStayTime;
        }
        setTimeout(() => {
          this.scrollTo(0);
          // reset state
          setTimeout(() => {
            this.state = '';
          }, 200);
        }, loadedStayTime);
      }
    }
  };
</script>

<style scoped>
  .vue-pull-to-wrapper {
    display: flex;
    flex-direction: column;
    height: 100%;
  }

  .scroll-container {
    flex: 1;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }

  .vue-pull-to-wrapper .action-block {
    position: relative;
    width: 100%;
  }

  .default-text {
    height: 100%;
    line-height: 50px;
    text-align: center;
  }
</style>
